/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.server;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.catalina.websocket.WsOutbound;
import org.apache.log4j.Logger;
import org.bmi.gwt.mi.server.core.queue.CallHandler;
import org.bmi.gwt.mi.server.core.queue.QueueState;
import org.bmi.gwt.mi.server.core.queue.QueuesManager;
import org.bmi.gwt.mi.server.core.queue.QueuesManager.BoundResult;
import org.bmi.gwt.mi.shared.communication.AttributeMessage;
import org.bmi.gwt.mi.shared.communication.AttributeMessage.MsgType;
import org.bmi.gwt.mi.shared.communication.CallsRequestMessage;
import org.bmi.gwt.mi.shared.communication.ErrorMessage;
import org.bmi.gwt.mi.shared.communication.FreeQueueMessage;
import org.bmi.gwt.mi.shared.communication.FreeQueueMessage.FreeMsgType;
import org.bmi.gwt.mi.shared.communication.ListenQueueMessage;
import org.bmi.gwt.mi.shared.communication.ListenQueueMessage.CreateMsgType;
import org.bmi.gwt.mi.shared.communication.Message;
import org.bmi.gwt.mi.shared.communication.attribute.AttributeValue;
import org.bmi.gwt.mi.shared.communication.call.Call;
import org.bmi.gwt.mi.shared.exceptions.InvalidCredentialsException;
import org.bmi.gwt.mi.shared.exceptions.NotAllowedQueueException;
import org.bmi.gwt.ws.server.AbstractWebSocketServer;
import org.bmi.gwt.ws.server.ObjectInbound;

class MiMessageInbound extends ObjectInbound<Message>
{
	static protected Logger logger = Logger.getLogger(MiMessageInbound.class);

	private QueuesManager queuesManager;

	private String credentials;

	private boolean alive;

	private Set<String> mayDiscard;

	private abstract class AbstractQueueState implements QueueState, CallHandler
	{
		private String queueName;
		private Map<String, AttributeValue<?>> attributes;

		AbstractQueueState(String queueName, Map<String, AttributeValue<?>> attributes)
		{
			this.queueName = queueName;
			this.attributes = attributes;
		}

		@Override
		public String getQueueName()
		{
			return this.queueName;
		}

		@Override
		public Map<String, AttributeValue<?>> getAttributes()
		{
			return this.attributes;
		}

		@Override
		public String getOwnerCredentials()
		{
			return MiMessageInbound.this.credentials;
		}

		@Override
		public boolean isReplaceable()
		{
			return !MiMessageInbound.this.alive;
		}

		@Override
		public boolean canConsumeCalls()
		{
			return true;
		}
	}

	public MiMessageInbound(AbstractWebSocketServer currentServlet, QueuesManager queuesManager, String credentials)
	{
		super(currentServlet);

		this.queuesManager = queuesManager;
		this.credentials = credentials;

		this.mayDiscard = new HashSet<String>();
	}

	@Override
	protected void onOpen(WsOutbound outbound)
	{
		/*
		 * Setting this flag to true, informing any generated queue by its state
		 * that they are no longer available to be bound.
		 */
		this.alive = true;

		logger.debug("[MiMessageInbound] connection enstablished for credentials " + this.credentials);
	}

	@Override
	protected void onClose(int status)
	{
		/*
		 * When the connection is closed, each queue descriptor will inform the
		 * queue that is not any longer alive.
		 */
		this.alive = false;

		/*
		 * Queues that should be discarded on disconnection should be discarded
		 * here.
		 */
		for (String queueName : this.mayDiscard)
			this.queuesManager.unbind(queueName, this.credentials, true);

		logger.debug("[MiMessageInbound] connection closed for credentials " + this.credentials);
	}

	@Override
	protected void onObjectMessage(Message msg)
	{
		String queueExpression = msg.getQueueExpression();

		if (msg instanceof ListenQueueMessage
				&& ((ListenQueueMessage) msg).getCreateMsgType() == CreateMsgType.ListenRequest)
		{
			/* Code responsible of opening a queue */
			Message r;

			ListenQueueMessage requestMsg = (ListenQueueMessage) msg;
			BoundResult br = this.queuesManager
					.bind(new AbstractQueueState(queueExpression, requestMsg.getAttributes())
					{
						@Override
						public void onCalls(String queueName, List<Call> elems)
						{
							if (MiMessageInbound.this.alive)
							{
								CallsRequestMessage response = new CallsRequestMessage();
								response.setCalls(elems);
								response.setQueueExpression(queueName);

								try
								{
									getWsOutboundWrapper().writeObjectMessage(response);
								}
								catch (IOException e)
								{
									logger.error("[MiMessageInbound] error while publishing:", e);
								}
							}
						}
					});

			if (br.success())
			{
				if (requestMsg.isDiscardOnDisconnect()) this.mayDiscard.add(queueExpression);

				ListenQueueMessage response = new ListenQueueMessage();
				response.setQueueExpression(queueExpression);
				response.setCreateMsgType(CreateMsgType.Listening);
				r = response;

				br.fireEvents();
			}
			else
			{
				ErrorMessage response = new ErrorMessage();
				response.setQueueExpression(queueExpression);
				response.setException(new NotAllowedQueueException("Queue \"" + queueExpression
						+ "\" can't be bound. Maybe it's already bound by another client."));
				r = response;
			}

			try
			{
				getWsOutboundWrapper().writeObjectMessage(r);
			}
			catch (IOException e)
			{
				logger.error("[MiMessageInbound] error while publishing:", e);
			}
		}
		else if (msg instanceof CallsRequestMessage)
		{
			this.queuesManager.publish(queueExpression, ((CallsRequestMessage) msg).getCalls());
		}
		else if (msg instanceof FreeQueueMessage
				&& ((FreeQueueMessage) msg).getFreeMsgType() == FreeMsgType.FreeRequest)
		{
			/*
			 * This message will free the queue, so there shouldn't be any risk
			 * that if the queue will be owned by some other client, it will be
			 * discarded on disconnection of current client.
			 */
			this.mayDiscard.remove(queueExpression);

			boolean discard = ((FreeQueueMessage) msg).isDiscard();

			this.queuesManager.unbind(queueExpression, this.credentials, discard);

			FreeQueueMessage response = new FreeQueueMessage();
			response.setQueueExpression(queueExpression);
			response.setFreeMsgType(FreeMsgType.Freed);

			try
			{
				getWsOutboundWrapper().writeObjectMessage(response);
			}
			catch (IOException e)
			{
				logger.error("[MiMessageInbound] error while publishing:", e);
			}
		}
		else if (msg instanceof AttributeMessage)
		{
			AttributeMessage<? extends AttributeValue<?>> attrMsg = (AttributeMessage<?>) msg;
			String key = attrMsg.getKey();
			AttributeValue<?> value = attrMsg.getValue();

			switch (attrMsg.getMsgType())
			{
			case RequestRemove:
				removeAttribute(queueExpression, key, attrMsg.getCallback());
				break;

			case RequestUpdate:
				updateAttribute(queueExpression, key, value, attrMsg.getCallback());
				break;

			case RequestValue:
				getAttribute(queueExpression, key, attrMsg.getCallback());
				break;

			default:
			}
		}
	}

	private <T extends AttributeValue<?>> void getAttribute(String queueName, String key, int callback)
	{
		T value = this.queuesManager.getQueueAttribute(queueName, key);

		AttributeMessage<T> response = new AttributeMessage<T>();
		response.setQueueExpression(queueName);
		response.setCallback(callback);
		response.setKey(key);
		response.setValue(value);
		response.setMsgType(MsgType.ValueProvided);

		try
		{
			getWsOutboundWrapper().writeObjectMessage(response);
		}
		catch (IOException e)
		{
			logger.error("[MiMessageInbound] error while publishing:", e);
		}
	}

	private <T extends AttributeValue<?>> void updateAttribute(String queueName, String key, T value, int callback)
	{
		Message response;

		if (this.queuesManager.setQueueAttribute(queueName, key, value, this.credentials))
		{
			AttributeMessage<T> attrMsg = new AttributeMessage<T>();
			attrMsg.setQueueExpression(queueName);
			attrMsg.setCallback(callback);
			attrMsg.setKey(key);
			attrMsg.setValue(value);
			attrMsg.setMsgType(MsgType.Updated);

			response = attrMsg;
		}
		else
		{
			ErrorMessage errMsg = new ErrorMessage();
			errMsg.setQueueExpression(queueName);
			errMsg.setCallback(callback);
			errMsg.setException(new InvalidCredentialsException("Impossible to update attribute of queue '" + queueName
					+ "' because provided credentials don't allow such operation."));

			response = errMsg;
		}

		try
		{
			getWsOutboundWrapper().writeObjectMessage(response);
		}
		catch (IOException e)
		{
			logger.error("[MiMessageInbound] error while publishing:", e);
		}
	}

	private <T extends AttributeValue<?>> void removeAttribute(String queueName, String key, int callback)
	{
		Message response;

		T value = this.queuesManager.getQueueAttribute(queueName, key);

		if (this.queuesManager.removeQueueAttribute(queueName, key, this.credentials))
		{
			AttributeMessage<T> attrMsg = new AttributeMessage<T>();
			attrMsg.setQueueExpression(queueName);
			attrMsg.setCallback(callback);
			attrMsg.setKey(key);
			attrMsg.setValue(value);
			attrMsg.setMsgType(MsgType.Removed);

			response = attrMsg;
		}
		else
		{
			ErrorMessage errMsg = new ErrorMessage();
			errMsg.setQueueExpression(queueName);
			errMsg.setCallback(callback);
			errMsg.setException(new InvalidCredentialsException("Impossible to update attribute of queue '" + queueName
					+ "' because provided credentials don't allow such operation."));

			response = errMsg;
		}

		try
		{
			getWsOutboundWrapper().writeObjectMessage(response);
		}
		catch (IOException e)
		{
			logger.error("[MiMessageInbound] error while publishing:", e);
		}
	}
}
